#!/usr/bin/env python3

#  Copyright © 2015 SpaghettiToastBook
#
#  This file is part of Dimension BX2.1.
#
#  Dimension BX2.1 is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  Dimension BX2.1 is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with Dimension BX2.1. If not, see <http://www.gnu.org/licenses/>.

from math import atan2, pi
import random
import cocos
import pyglet.clock
from pyglet.image import Animation
import pyglet.media
import pymunk
from pymunk.vec2d import Vec2d
from gamewide import *
from controls import *

"""Module containing the game scene."""

__all__ = ['GameScene', 'counter']

tau = 2 * pi

def polar_Vec2d(magnitude, angle):
    vector = Vec2d(1, 0)
    vector.angle = angle
    vector.length = magnitude
    return vector

class AccessCounter:

    def __init__(self):
        self.count = 0

    @property
    def count(self):
        return self._count

    @count.setter
    def count(self, value):
        self._count = value
        assert self._count >= 0

    def __enter__(self):
        self.count += 1

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.count -= 1
        return False

counter = AccessCounter()

class PlayerShip(cocos.sprite.Sprite):

    z = -2
    collision_type = 1

    movement_speed = 200
    bullet_distance = 6
    bullet_speed = 350

    ani = Animation.from_image_sequence([loader.image('Player/Ship (0).png'),
                                         loader.image('Player/Ship (1).png')],
                                        1 / 8)

    def __init__(self, position):
        super().__init__(self.ani, position=tuple(position),
                         anchor=(self.ani.get_max_width() / 2,
                                 self.ani.get_max_height() / 2))

        self.body = pymunk.Body(mass=1, moment=pymunk.inf)
        self.body.position = position
        self.shape = pymunk.Circle(self.body, radius=self.width / 4)
        self.shape.collision_type = self.collision_type

        self.alive = True

    def on_up_press(self):
        counter.count += 1
        self.body.apply_impulse((0, self.movement_speed))
    def on_down_press(self):
        counter.count += 1
        self.body.apply_impulse((0, -self.movement_speed))
    def on_right_press(self):
        counter.count += 1
        self.body.apply_impulse((self.movement_speed, 0))
    def on_left_press(self):
        counter.count += 1
        self.body.apply_impulse((-self.movement_speed, 0))

    def on_up_release(self):
        self.body.apply_impulse((0, -self.movement_speed))
        counter.count -= 1
    def on_down_release(self):
        self.body.apply_impulse((0, self.movement_speed))
        counter.count -= 1
    def on_right_release(self):
        self.body.apply_impulse((-self.movement_speed, 0))
        counter.count -= 1
    def on_left_release(self):
        self.body.apply_impulse((self.movement_speed, 0))
        counter.count -= 1

    def update(self):
        right_bound = window_width - (self.width // 2)
        left_bound = self.width // 2
        up_bound = window_height - (self.height // 2)
        down_bound = self.height // 2
        if self.body.position.x > right_bound:
            self.body.position.x = right_bound
        elif self.body.position.x < left_bound:
            self.body.position.x = left_bound
        if self.body.position.y > up_bound:
            self.body.position.y = up_bound
        elif self.body.position.y < down_bound:
            self.body.position.y = down_bound

        self.position = self.body.position

    def die(self):
        self.alive = False
        explosion_sound.play()
        self.parent.space.remove(self.body, self.shape)
        self.kill()
        self.visible = False # Replace with explosion

    def on_fire(self, x, y):
        with counter:
            bullet_angle = atan2(*reversed((x, y) - self.body.position))
            bullet_position = (polar_Vec2d(self.bullet_distance, bullet_angle) +
                           self.body.position)
            bullet = PlayerBullet(bullet_position, bullet_angle)
            bullet_sound.play()
            self.parent.add(bullet, z=bullet.z)
            self.parent.space.add(bullet.body, bullet.shape)
            bullet.body.apply_impulse(polar_Vec2d(self.bullet_speed,
                                                  bullet_angle))

class PlayerBullet(cocos.sprite.Sprite):

    z = -1
    collision_type = 2

    ani = Animation.from_image_sequence([loader.image('Player/Bullet (0).png'),
                                         loader.image('Player/Bullet (1).png')],
                                        1 / 8)

    def __init__(self, position, angle):
        super().__init__(self.ani, position=tuple(position),
                         anchor=(self.ani.get_max_width() / 2,
                                 self.ani.get_max_height() / 2))

        self.body = pymunk.Body(mass=1, moment=pymunk.inf)
        self.body.position = position
        self.shape = pymunk.Circle(self.body, radius=self.width / 2)
        self.shape.collision_type = self.collision_type

    def update(self):
        right_bound = window_width + (self.width // 2)
        left_bound = -(self.width // 2)
        up_bound = window_height + (self.height // 2)
        down_bound = -(self.height // 2)
        if self.body.position.x > right_bound:
            self.body.position.x = left_bound
        elif self.body.position.x < left_bound:
            self.body.position.x = right_bound
        if self.body.position.y > up_bound:
            self.body.position.y = down_bound
        elif self.body.position.y < down_bound:
            self.body.position.y = up_bound

        self.position = self.body.position

class EnemyShip(cocos.sprite.Sprite):

    z = 0
    collision_type = 3

    movement_speed = 75
    bullet_delay = 1.5
    initial_bullet_speed = 200
    bullet_speed_multiplier = 1.1
    mega_fire_probability = 0.3
    fast_bullet_speed_multiplier = 2

    ani = Animation.from_image_sequence([loader.image('Enemy/Ship (0).png'),
                                         loader.image('Enemy/Ship (1).png')],
                                        1 / 8)

    def __init__(self, position):
        super().__init__(self.ani, position=tuple(position),
                         anchor=(self.ani.get_max_width() / 2,
                                 self.ani.get_max_height() / 2))

        self.body = pymunk.Body(mass=1, moment=pymunk.inf)
        self.body.position = position
        self.shape = pymunk.Circle(self.body, radius=self.width / 2)
        self.shape.collision_type = self.collision_type

        self.body.apply_impulse(polar_Vec2d(self.movement_speed,
                                            random.uniform(0, tau)))
        self.bullet_speed = self.initial_bullet_speed
        pyglet.clock.schedule_interval(self.fire, self.bullet_delay)

    def update(self):
        right_bound = window_width + (self.width // 2)
        left_bound = -(self.width // 2)
        up_bound = window_height + (self.height // 2)
        down_bound = -(self.height // 2)
        if self.body.position.x > right_bound:
            self.body.position.x = left_bound
        elif self.body.position.x < left_bound:
            self.body.position.x = right_bound
        if self.body.position.y > up_bound:
            self.body.position.y = down_bound
        elif self.body.position.y < down_bound:
            self.body.position.y = up_bound

        self.position = self.body.position

    def on_exit(self):
        pyglet.clock.unschedule(self.fire)

    def die(self):
        pyglet.clock.unschedule(self.fire)
        self.parent.score += 500
        explosion_sound.play()
        self.mega_fire()
        self.kill()
        self.parent.space.remove(self.body, self.shape)

    def mega_fire(self, dt=None):
        def fast_shot():
            self.bullet_speed *= self.fast_bullet_speed_multiplier
            self.fire(no_mega_fire=True)
            self.bullet_speed /= self.fast_bullet_speed_multiplier
        def sixteen_way_shot():
            for angle in (n * (tau / 16) for n in range(16)):
                self.fire(bullet_angle=angle, no_mega_fire=True,
                          multiply=False)
            self.bullet_speed *= self.bullet_speed_multiplier
        with counter:
            {0: fast_shot, 1: sixteen_way_shot}[random.randint(0, 1)]()


    def fire(self, dt=None, bullet_angle=None, no_mega_fire=False,
             multiply=True):
        if not no_mega_fire and random.random() < self.mega_fire_probability:
            self.mega_fire()
        else:
            with counter:
                if bullet_angle is None:
                    bullet_angle = atan2(
                                      *reversed(
                                        self.parent.player_ship.body.position -
                                        self.body.position))
                bullet_position = self.body.position
                bullet = EnemyBullet(bullet_position, bullet_angle)
                self.parent.add(bullet, z=bullet.z)
                self.parent.space.add(bullet.body, bullet.shape)
                bullet.body.apply_impulse(polar_Vec2d(self.bullet_speed,
                                                      bullet_angle))
                if multiply:
                    self.bullet_speed *= self.bullet_speed_multiplier

class EnemyBullet(cocos.sprite.Sprite):

    z = PlayerBullet.z
    collision_type = 4

    ani = Animation.from_image_sequence([loader.image('Enemy/Bullet (0).png'),
                                         loader.image('Enemy/Bullet (1).png')],
                                        1 / 8)

    def __init__(self, position, angle):
        super().__init__(self.ani, position=tuple(position),
                         anchor=(self.ani.get_max_width() / 2,
                                 self.ani.get_max_height() / 2))

        self.body = pymunk.Body(mass=1, moment=pymunk.inf)
        self.body.position = position
        self.shape = pymunk.Circle(self.body, radius=self.width / 2)
        self.shape.collision_type = self.collision_type

    def die(self):
        self.parent.space.remove(self.body, self.shape)
        self.kill()

    def update(self):
        self.position = self.body.position

        right_bound = window_width + (self.width // 2)
        left_bound = -(self.width // 2)
        up_bound = window_height + (self.height // 2)
        down_bound = -(self.height // 2)
        if self.body.position.x > right_bound or \
           self.body.position.x < left_bound or \
           self.body.position.y > up_bound or \
           self.body.position.y < down_bound:
            self.die()

class GameLayer(cocos.layer.Layer):

    is_event_handler = True

    enemy_delay = 3
    enemy_delay_multiplier = 0.9

    text_z = 3

    def __init__(self):
        super().__init__()
        self.space = pymunk.Space()
        def nothing_handler(space, arbiter):
            return False
        self.space.set_default_collision_handler(begin=nothing_handler)
        self.space.add_collision_handler(1, 2, begin=self.player_death_handler)
        self.space.add_collision_handler(1, 3, begin=self.player_death_handler)
        self.space.add_collision_handler(1, 4, begin=self.player_death_handler)
        self.space.add_collision_handler(3, 2, begin=self.enemy_death_handler)

        self.player_ship = PlayerShip((window_width // 2, window_height // 2))
        self.add(self.player_ship, z=self.player_ship.z)
        self.space.add(self.player_ship.body, self.player_ship.shape)
        controls.push_handlers(self.player_ship)

        self.enemy_shape_dict = dict()

        self.score = 0
        self.score_label = cocos.text.Label('Score:0', font_name=font_name,
                                            font_size=font_size,
                                            color=font_color, anchor_x='left',
                                            anchor_y='top')
        self.score_label.position = (4, window_height - 3)
        self.add(self.score_label, z=self.text_z)

    def on_enter(self):
        pyglet.clock.schedule_interval(self.update, 1 / fps)
        pyglet.clock.schedule_interval(self.generate_enemy, self.enemy_delay)

    def player_death_handler(self, space, arbiter):
        self.player_ship.die()
        return False

    def enemy_death_handler(self, space, arbiter):
        self.enemy_shape_dict[arbiter.shapes[0]].die()
        del self.enemy_shape_dict[arbiter.shapes[0]]
        return False

    def on_exit(self):
        self.end(show_score=False)

    def end(self, show_score=True):
        controls.remove_handlers(self.player_ship)
        pyglet.clock.unschedule(self.update)
        pyglet.clock.unschedule(self.generate_enemy)
        for body, shape in zip(self.space.bodies, self.space.shapes):
            self.space.remove(body, shape)

    def update(self, dt):
        dt = 1 / fps
        self.score += 1
        self.score_label.element.text = 'Score:{}'.format(self.score)

        for thing in self.get_children():
            if hasattr(thing, 'update'):
                thing.update()
        self.space.step(dt)

    def generate_enemy(self, dt):
        enemy_position = self.player_ship.body.position
        while self.player_ship.shape.point_query(enemy_position):
            enemy_position = (random.randint(0, window_width),
                              random.randint(0, window_height))
        enemy = EnemyShip(enemy_position)
        self.add(enemy, z=enemy.z)
        self.space.add(enemy.body, enemy.shape)
        self.enemy_shape_dict[enemy.shape] = enemy

class GameScene(cocos.scene.Scene):

    def __init__(self):
        self.game_layer = GameLayer()
        super().__init__(self.game_layer)

    def on_exit(self):
        self.remove(self.game_layer)

